<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body{
            margin: 10px;
        }
        .toolbar{
            background: #ddd;
            height:40px;
            line-height: 40px;
            padding: 0 4px;
        }
        .editor{
            min-height: 400px;
            border: 4px solid #ddd;
            font-size: 2rem;
            font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
        }
    </style>
</head>

<body>
    <div class="toolbar">
        <button data-cmd="Bold">B</button>
        <button data-cmd="Italic">I</button>
        <button data-cmd="UnderLine">U</button>
        <button data-cmd="HighLight">H</button>
        <button data-cmd="undo">undo</button>


        <span>|</span>
        <button data-cmd="Save" id="save">Save</button>
    </div>
    <div class="editor" contenteditable="true">
        Lorem ipsum dolor sit, amet consectetur adipisicing elit. Vero nulla minima similique. Accusamus alias dolore cumque explicabo, nostrum quisquam rerum quaerat provident voluptatibus illo error earum sed nisi vel! Illum.
    </div>

    <div class="checkpoint"></div>

    <script>
        class Command{
            constructor(editor){
                this.editor = editor;
                this.state = "";
            }
            Execute(){
                this.state = this.editor.getHTML();
                this.exec();
            }
            undo(){
                this.editor.setHTML(this.state);
            }
            getState(){
                return this.state;
            }

        }
        class BoldCommand extends Command{
            exec(){
                this.editor.Bold();
            }
        }
        class ItalicCommand extends Command{
            exec(){
                this.editor.Italic();
            }
        }
        class UnderLineCommand extends Command{
            exec(){
                this.editor.UnderLine();
            }
        }
        class HighLightCommand extends Command{
            exec(){
                this.editor.HighLight();
            }
        }

        class SaveCommand extends Command{
            exec(){
                alert("saved!")
            }
        }


        class CheckPoint{
            constructor(state){
                this.state = state;
            }
            getState(){
                return this.state;
            }
            
        }

        class CheckPointHistory{
            constructor(render){
                this.checkPoints = [];
                this.render = render;
            }
            addCheckPoint(checkpoint){
                const name = `# ${this.checkPoints.length} checkPoint`;
                const state = {name:name,checkpoint}
                this.checkPoints.push(state);

                this.render.render(this);
            }
            get(index){
                return this.checkPoints[index].checkpoint;
            }
        }

        class CheckPointRender{
            constructor(el,execute){
                this.el = el;
                this.execute = execute;
            }

            render(history){
                this.el.innerHTML = "";
                history.checkPoints.forEach((item,idx)=>{
                    const div = document.createElement("div");
                    div.innerHTML = `# ${item.name}`
                    div.setAttribute("data-idx",idx)

                    this.el.append(div);

                    div.addEventListener("click",(e)=>{
                        const idx = e.target.dataset.idx;
                        this.execute.restore(history.get(idx));
                    })
                })
            }
        }

       

        class Editor{
            constructor(el){
                this.el = el;
            }
            wrapElement(nodeName,style){
                const selec = window.getSelection();
                const focusEle =selec.focusNode.nodeType === Node.TEXT_NODE?
                selec.focusNode.parentElement:selec.focusNode;

                if(focusEle.nodeName === nodeName.toUpperCase()){
                    focusEle.outerHTML = focusEle.innerHTML;
                }
                else{
                    const range = selec.getRangeAt(0);
                    const node = document.createElement(nodeName);
                    style && (node.style = style)
                    range.surroundContents(node);

                    const newRange = document.createRange();
                    newRange.selectNodeContents(node);

                    selec.removeAllRanges();
                    selec.addRange(newRange);
                }
            }

            Bold(){
                this.wrapElement("b");
            }
            Italic(){
                this.wrapElement("i");
            }
            UnderLine(){
                this.wrapElement("u");
            }
            HighLight(){
                this.wrapElement("span","background:yellow;")
            }

            getHTML(){
                return this.el.innerHTML;
            }
            setHTML(value){
                this.el.innerHTML = value;
            }
        }

        class CommandExector{
            constructor(){
                this.factory ={};
                this.history =[];
            }
            registerCommmand(name,func){
                this.factory[name] = func;
            }
            exec(name){
                const cmd = this.factory[name]();
                cmd.Execute();
                this.history.push(cmd);
            }
            undo(){
                const popCmd = this.history.pop();
                if(!popCmd) return;

                popCmd.undo();
            }
            saveCheckPoint(){
                return new CheckPoint(this.history);
            }
            restore(chkpoint){
                let history = [...chkpoint.getState()];
                console.log(history)
                this.history = history;
                this.undo();
                
            }
        }

        const editor = new Editor(document.querySelector(".editor"));
        const commexec = new CommandExector();
        commexec.registerCommmand("Bold",()=>{return new BoldCommand(editor)})
        commexec.registerCommmand("Italic",()=>{return new ItalicCommand(editor)})
        commexec.registerCommmand("UnderLine",()=>{return new UnderLineCommand(editor)})
        commexec.registerCommmand("HighLight",()=>{return new HighLightCommand(editor)})
        commexec.registerCommmand("Save",()=>new SaveCommand(editor))

        document.querySelectorAll("button").forEach(item=>{
            item.addEventListener("click",e=>{
                const cmd = e.target.dataset.cmd;
                if(cmd ==="undo"){
                    commexec.undo();
                }
                else{
                    commexec.exec(cmd);
                }
            })
        })

        const render = new CheckPointRender(document.querySelector(".checkpoint"),commexec);
        const checkPointHistory = new CheckPointHistory(render);
        
        document.querySelector("#save").addEventListener("click",e=>{
            checkPointHistory.addCheckPoint(commexec.saveCheckPoint());
        })
    </script>
</body>
</html>